/* * Licensed to DuraSpace under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * DuraSpace licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fcrepo.kernel.modeshape; import static java.time.Instant.ofEpochMilli; import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static java.util.stream.Stream.concat; import static java.util.stream.Stream.empty; import static java.util.stream.Stream.of; import static org.apache.commons.codec.digest.DigestUtils.sha1Hex; import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; import static org.apache.jena.update.UpdateAction.execute; import static org.apache.jena.update.UpdateFactory.create; import static org.fcrepo.kernel.api.RdfCollectors.toModel; import static org.fcrepo.kernel.api.RdfLexicon.LAST_MODIFIED_DATE; import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; import static org.fcrepo.kernel.api.RdfLexicon.isManagedNamespace; import static org.fcrepo.kernel.api.RdfLexicon.isManagedPredicate; import static org.fcrepo.kernel.api.RequiredRdfContext.EMBED_RESOURCES; import static org.fcrepo.kernel.api.RequiredRdfContext.INBOUND_REFERENCES; import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT; import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP; import static org.fcrepo.kernel.api.RequiredRdfContext.MINIMAL; import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES; import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED; import static org.fcrepo.kernel.api.RequiredRdfContext.VERSIONS; import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.FROZEN_MIXIN_TYPES; import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_CREATED; import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.JCR_LASTMODIFIED; import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT; import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.jcrProperties; import static org.fcrepo.kernel.modeshape.identifiers.NodeResourceConverter.nodeConverter; import static org.fcrepo.kernel.modeshape.rdf.JcrRdfTools.getRDFNamespaceForJcrNamespace; import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.isFrozen; import static org.fcrepo.kernel.modeshape.services.functions.JcrPropertyFunctions.property2values; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getContainingNode; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.hasInternalNamespace; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isFrozenNode; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.isInternalNode; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.ldpInsertedContentProperty; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.resourceToProperty; import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.touchLdpMembershipResource; import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry; import static org.fcrepo.kernel.modeshape.utils.StreamUtils.iteratorToStream; import static org.fcrepo.kernel.modeshape.utils.UncheckedFunction.uncheck; import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT; import static org.slf4j.LoggerFactory.getLogger; import java.net.URI; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.jcr.ItemNotFoundException; import javax.jcr.NamespaceRegistry; import javax.jcr.Node; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.nodetype.NodeType; import javax.jcr.version.Version; import javax.jcr.version.VersionHistory; import javax.jcr.version.VersionManager; import org.fcrepo.kernel.api.FedoraTypes; import org.fcrepo.kernel.api.FedoraVersion; import org.fcrepo.kernel.api.RdfStream; import org.fcrepo.kernel.api.TripleCategory; import org.fcrepo.kernel.api.exception.AccessDeniedException; import org.fcrepo.kernel.api.exception.ConstraintViolationException; import org.fcrepo.kernel.api.exception.InvalidPrefixException; import org.fcrepo.kernel.api.exception.MalformedRdfException; import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException; import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; import org.fcrepo.kernel.api.identifiers.IdentifierConverter; import org.fcrepo.kernel.api.models.FedoraResource; import org.fcrepo.kernel.api.rdf.DefaultRdfStream; import org.fcrepo.kernel.api.utils.GraphDifferencer; import org.fcrepo.kernel.modeshape.rdf.converters.PropertyConverter; import org.fcrepo.kernel.modeshape.rdf.impl.AclRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.ChildrenRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.ContentRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.HashRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.LdpContainerRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.LdpIsMemberOfRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.LdpRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.ParentRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.PropertiesRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.ReferencesRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.RootRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.SkolemNodeRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.TypeRdfContext; import org.fcrepo.kernel.modeshape.rdf.impl.VersionsRdfContext; import org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils; import org.fcrepo.kernel.modeshape.utils.JcrPropertyStatementListener; import org.fcrepo.kernel.modeshape.utils.PropertyChangedListener; import org.fcrepo.kernel.modeshape.utils.UncheckedPredicate; import org.fcrepo.kernel.modeshape.utils.iterators.RdfAdder; import org.fcrepo.kernel.modeshape.utils.iterators.RdfRemover; import org.apache.jena.graph.Triple; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.sparql.core.Quad; import org.apache.jena.sparql.modify.request.UpdateData; import org.apache.jena.sparql.modify.request.UpdateDeleteWhere; import org.apache.jena.sparql.modify.request.UpdateModify; import org.apache.jena.update.UpdateRequest; import org.modeshape.jcr.api.JcrTools; import org.slf4j.Logger; import com.google.common.base.Converter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; /** * Common behaviors across {@link org.fcrepo.kernel.api.models.Container} and * {@link org.fcrepo.kernel.api.models.NonRdfSourceDescription} types; also used * when the exact type of an object is irrelevant * * @author ajs6f */ public class FedoraResourceImpl extends JcrTools implements FedoraTypes, FedoraResource { private static final Logger LOGGER = getLogger(FedoraResourceImpl.class); private static final long NO_TIME = 0L; private static final String JCR_CHILD_VERSION_HISTORY = "jcr:childVersionHistory"; private static final String JCR_VERSIONABLE_UUID = "jcr:versionableUuid"; private static final String JCR_FROZEN_UUID = "jcr:frozenUuid"; private static final String JCR_VERSION_STORAGE = "jcr:versionStorage"; private static final PropertyConverter propertyConverter = new PropertyConverter(); // A curried type accepting resource, translator, and "minimality", returning triples. private static interface RdfGenerator extends Function<FedoraResource, Function<IdentifierConverter<Resource, FedoraResource>, Function<Boolean, Stream<Triple>>>> {} @SuppressWarnings("resource") private static RdfGenerator getDefaultTriples = resource -> translator -> uncheck(minimal -> { final Stream<Stream<Triple>> min = of( new TypeRdfContext(resource, translator), new PropertiesRdfContext(resource, translator)); if (!minimal) { final Stream<Stream<Triple>> extra = of( new HashRdfContext(resource, translator), new SkolemNodeRdfContext(resource, translator)); return concat(min, extra).reduce(empty(), Stream::concat); } return min.reduce(empty(), Stream::concat); }); private static RdfGenerator getEmbeddedResourceTriples = resource -> translator -> uncheck(minimal -> resource.getChildren().flatMap(child -> child.getTriples(translator, PROPERTIES))); private static RdfGenerator getInboundTriples = resource -> translator -> uncheck(_minimal -> { return new ReferencesRdfContext(resource, translator); }); private static RdfGenerator getLdpContainsTriples = resource -> translator -> uncheck(_minimal -> { return new ChildrenRdfContext(resource, translator); }); private static RdfGenerator getVersioningTriples = resource -> translator -> uncheck(_minimal -> { return new VersionsRdfContext(resource, translator); }); @SuppressWarnings("resource") private static RdfGenerator getServerManagedTriples = resource -> translator -> uncheck(minimal -> { if (minimal) { return new LdpRdfContext(resource, translator); } final Stream<Stream<Triple>> streams = of( new LdpRdfContext(resource, translator), new AclRdfContext(resource, translator), new RootRdfContext(resource, translator), new ContentRdfContext(resource, translator), new ParentRdfContext(resource, translator)); return streams.reduce(empty(), Stream::concat); }); @SuppressWarnings("resource") private static RdfGenerator getLdpMembershipTriples = resource -> translator -> uncheck(_minimal -> { final Stream<Stream<Triple>> streams = of( new LdpContainerRdfContext(resource, translator), new LdpIsMemberOfRdfContext(resource, translator)); return streams.reduce(empty(), Stream::concat); }); private static final Map<TripleCategory, RdfGenerator> contextMap = ImmutableMap.<TripleCategory, RdfGenerator>builder() .put(PROPERTIES, getDefaultTriples) .put(VERSIONS, getVersioningTriples) .put(EMBED_RESOURCES, getEmbeddedResourceTriples) .put(INBOUND_REFERENCES, getInboundTriples) .put(SERVER_MANAGED, getServerManagedTriples) .put(LDP_MEMBERSHIP, getLdpMembershipTriples) .put(LDP_CONTAINMENT, getLdpContainsTriples) .build(); protected Node node; /* * A terminating slash means ModeShape has trouble extracting the localName, e.g., for http://myurl.org/. * * @see <a href="https://jira.duraspace.org/browse/FCREPO-1409"> FCREPO-1409 </a> for details. */ private static final Function<Quad, IllegalArgumentException> validatePredicateEndsWithSlash = uncheck(x -> { if (x.getPredicate().isURI() && x.getPredicate().getURI().endsWith("/")) { return new IllegalArgumentException("Invalid predicate ends with '/': " + x.getPredicate().getURI()); } return null; }); /* * Ensures the object URI is valid */ private static final Function<Quad, IllegalArgumentException> validateObjectUrl = uncheck(x -> { if (x.getObject().isURI()) { final String uri = x.getObject().toString(); try { new URI(uri); } catch (Exception ex) { return new IllegalArgumentException("Invalid object URI (" + uri + " ) : " + ex.getMessage()); } } return null; }); private static final List<Function<Quad, IllegalArgumentException>> quadValidators = ImmutableList.<Function<Quad, IllegalArgumentException>>builder() .add(validatePredicateEndsWithSlash) .add(validateObjectUrl).build(); /** * Construct a {@link org.fcrepo.kernel.api.models.FedoraResource} from an existing JCR Node * @param node an existing JCR node to treat as an fcrepo object */ public FedoraResourceImpl(final Node node) { this.node = node; } /** * Return the underlying JCR Node for this resource * * @return the JCR Node */ public Node getNode() { return node; } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getPath() */ @Override public String getPath() { try { final String path = node.getPath(); return path.endsWith("/" + JCR_CONTENT) ? path.substring(0, path.length() - JCR_CONTENT.length() - 1) : path; } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getChildren(Boolean recursive) */ @Override public Stream<FedoraResource> getChildren(final Boolean recursive) { try { if (recursive) { return nodeToGoodChildren(node).flatMap(FedoraResourceImpl::getAllChildren); } return nodeToGoodChildren(node); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getDescription() */ @Override public FedoraResource getDescription() { return this; } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getDescribedResource() */ @Override public FedoraResource getDescribedResource() { return this; } /** * Get the "good" children for a node by skipping all pairtree nodes in the way. * @param input * @return * @throws RepositoryException */ @SuppressWarnings("unchecked") private Stream<FedoraResource> nodeToGoodChildren(final Node input) throws RepositoryException { return iteratorToStream(input.getNodes()).filter(nastyChildren.negate()) .flatMap(uncheck((final Node child) -> child.isNodeType(FEDORA_PAIRTREE) ? nodeToGoodChildren(child) : of(nodeToObjectBinaryConverter.convert(child)))); } /** * Get all children recursively, and flatten into a single Stream. */ private static Stream<FedoraResource> getAllChildren(final FedoraResource resource) { return concat(of(resource), resource.getChildren().flatMap(FedoraResourceImpl::getAllChildren)); } /** * Children for whom we will not generate triples. */ private static Predicate<Node> nastyChildren = isInternalNode .or(TombstoneImpl::hasMixin) .or(UncheckedPredicate.uncheck(p -> p.getName().equals(JCR_CONTENT))) .or(UncheckedPredicate.uncheck(p -> p.getName().equals("#"))); private static final Converter<FedoraResource, FedoraResource> datastreamToBinary = new Converter<FedoraResource, FedoraResource>() { @Override protected FedoraResource doForward(final FedoraResource fedoraResource) { return fedoraResource.getDescribedResource(); } @Override protected FedoraResource doBackward(final FedoraResource fedoraResource) { return fedoraResource.getDescription(); } }; private static final Converter<Node, FedoraResource> nodeToObjectBinaryConverter = nodeConverter.andThen(datastreamToBinary); @Override public FedoraResource getContainer() { return getContainingNode(getNode()).map(nodeConverter::convert).orElse(null); } @Override public FedoraResource getChild(final String relPath) { try { return nodeConverter.convert(getNode().getNode(relPath)); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public boolean hasProperty(final String relPath) { try { return getNode().hasProperty(relPath); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public void delete() { try { // Remove inbound references to this resource and, recursively, any of its children removeReferences(node); final Node parent = getNode().getDepth() > 0 ? getNode().getParent() : null; final String name = getNode().getName(); // This is resolved immediately b/c we delete the node before updating an indirect container's target final boolean shouldUpdateIndirectResource = ldpInsertedContentProperty(node) .flatMap(resourceToProperty(getSession())).filter(this::hasProperty).isPresent(); final Optional<Node> containingNode = getContainingNode(getNode()); node.remove(); if (parent != null) { createTombstone(parent, name); // also update membershipResources for Direct/Indirect Containers containingNode.filter(UncheckedPredicate.uncheck((final Node ancestor) -> ancestor.hasProperty(LDP_MEMBER_RESOURCE) && (ancestor.isNodeType(LDP_DIRECT_CONTAINER) || shouldUpdateIndirectResource))) .ifPresent(ancestor -> { try { FedoraTypesUtils.touch(ancestor.getProperty(LDP_MEMBER_RESOURCE).getNode()); } catch (final RepositoryException ex) { throw new RepositoryRuntimeException(ex); } }); // update the lastModified date on the parent node containingNode.ifPresent(ancestor -> { FedoraTypesUtils.touch(ancestor); }); } } catch (final javax.jcr.AccessDeniedException e) { throw new AccessDeniedException(e); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } private void removeReferences(final Node n) { try { // Remove references to this resource doRemoveReferences(n); // Recurse over children of this resource if (n.hasNodes()) { @SuppressWarnings("unchecked") final Iterator<Node> nodes = n.getNodes(); nodes.forEachRemaining(this::removeReferences); } } catch (RepositoryException e) { throw new RepositoryRuntimeException(e); } } private void doRemoveReferences(final Node n) throws RepositoryException { @SuppressWarnings("unchecked") final Iterator<Property> references = n.getReferences(); @SuppressWarnings("unchecked") final Iterator<Property> weakReferences = n.getWeakReferences(); concat(iteratorToStream(references), iteratorToStream(weakReferences)).forEach(prop -> { try { final List<Value> newVals = property2values.apply(prop).filter( UncheckedPredicate.uncheck(value -> !n.equals(getSession().getNodeByIdentifier(value.getString())))) .collect(toList()); if (newVals.size() == 0) { prop.remove(); } else { prop.setValue(newVals.toArray(new Value[newVals.size()])); } } catch (final RepositoryException ex) { // Ignore error from trying to update properties on versioned resources if (ex instanceof javax.jcr.nodetype.ConstraintViolationException && ex.getMessage().contains(JCR_VERSION_STORAGE)) { LOGGER.debug("Ignoring exception trying to remove property from versioned resource: {}", ex.getMessage()); } else { throw new RepositoryRuntimeException(ex); } } }); } private void createTombstone(final Node parent, final String path) throws RepositoryException { findOrCreateChild(parent, path, FEDORA_TOMBSTONE); } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getCreatedDate() */ @Override public Instant getCreatedDate() { try { if (hasProperty(JCR_CREATED)) { return ofEpochMilli(getTimestamp(JCR_CREATED, NO_TIME)); } } catch (final PathNotFoundException e) { throw new PathNotFoundRuntimeException(e); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } LOGGER.debug("Node {} does not have a createdDate", node); return null; } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getLastModifiedDate() */ /** * This method gets the last modified date for this FedoraResource. Because * the last modified date is managed by fcrepo (not ModeShape) while the created * date *is* managed by ModeShape in the current implementation it's possible that * the last modified date will be before the created date. Instead of making * a second update to correct the modified date, in cases where the modified * date is ealier than the created date, this class presents the created date instead. * * Any method that exposes the last modified date must maintain this illusion so * that that external callers are presented with a sensible and consistent * representation of this resource. * @return the last modified Instant (or the created Instant if it was after the last * modified date) */ @Override public Instant getLastModifiedDate() { final Instant createdDate = getCreatedDate(); try { final long created = createdDate == null ? NO_TIME : createdDate.toEpochMilli(); if (hasProperty(FEDORA_LASTMODIFIED)) { return ofEpochMilli(getTimestamp(FEDORA_LASTMODIFIED, created)); } else if (hasProperty(JCR_LASTMODIFIED)) { return ofEpochMilli(getTimestamp(JCR_LASTMODIFIED, created)); } } catch (final PathNotFoundException e) { throw new PathNotFoundRuntimeException(e); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } LOGGER.debug("Could not get last modified date property for node {}", node); if (createdDate != null) { LOGGER.trace("Using created date for last modified date for node {}", node); return createdDate; } return null; } private long getTimestamp(final String property, final long created) throws RepositoryException { LOGGER.trace("Using {} date", property); final long timestamp = getProperty(property).getDate().getTimeInMillis(); if (timestamp < created && created > NO_TIME) { LOGGER.trace("Returning the later created date ({} > {}) for {}", created, timestamp, property); return created; } return timestamp; } /** * Set the last-modified date to the current date. */ public void touch() { FedoraTypesUtils.touch(getNode()); } @Override public boolean hasType(final String type) { try { if (type.equals(FEDORA_REPOSITORY_ROOT)) { return node.isNodeType(ROOT); } else if (isFrozen.test(node) && hasProperty(FROZEN_MIXIN_TYPES)) { return property2values.apply(getProperty(FROZEN_MIXIN_TYPES)).map(uncheck(Value::getString)) .anyMatch(type::equals); } return node.isNodeType(type); } catch (final PathNotFoundException e) { throw new PathNotFoundRuntimeException(e); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public List<URI> getTypes() { try { final List<NodeType> nodeTypes = new ArrayList<>(); final NodeType primaryNodeType = node.getPrimaryNodeType(); nodeTypes.add(primaryNodeType); nodeTypes.addAll(asList(primaryNodeType.getSupertypes())); final List<NodeType> mixinTypes = asList(node.getMixinNodeTypes()); nodeTypes.addAll(mixinTypes); mixinTypes.stream() .map(NodeType::getSupertypes) .flatMap(Arrays::stream) .forEach(nodeTypes::add); final List<URI> types = nodeTypes.stream() .map(uncheck(NodeType::getName)) .filter(hasInternalNamespace.negate()) .distinct() .map(nodeTypeNameToURI) .peek(x -> LOGGER.debug("node has rdf:type {}", x)) .collect(Collectors.toList()); if (isFrozenResource()) { types.add(URI.create(REPOSITORY_NAMESPACE + "Version")); } return types; } catch (final PathNotFoundException e) { throw new PathNotFoundRuntimeException(e); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } private final Function<String, URI> nodeTypeNameToURI = uncheck(name -> { final String prefix = name.split(":")[0]; final String typeName = name.split(":")[1]; final String namespace = getSession().getWorkspace().getNamespaceRegistry().getURI(prefix); return URI.create(getRDFNamespaceForJcrNamespace(namespace) + typeName); }); /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#updateProperties * (org.fcrepo.kernel.api.identifiers.IdentifierConverter, java.lang.String, RdfStream) */ @Override public void updateProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, final String sparqlUpdateStatement, final RdfStream originalTriples) throws MalformedRdfException, AccessDeniedException { final Model model = originalTriples.collect(toModel()); final UpdateRequest request = create(sparqlUpdateStatement, idTranslator.reverse().convert(this).toString()); final Collection<IllegalArgumentException> errors = validateUpdateRequest(request); final NamespaceRegistry namespaceRegistry = getNamespaceRegistry(getSession()); request.getPrefixMapping().getNsPrefixMap().forEach( (k,v) -> { try { LOGGER.debug("Prefix mapping is key:{} -> value:{}", k, v); if (Arrays.asList(namespaceRegistry.getPrefixes()).contains(k) && !v.equals(namespaceRegistry.getURI(k))) { final String namespaceURI = namespaceRegistry.getURI(k); LOGGER.debug("Prefix has already been defined: {}:{}", k, namespaceURI); throw new InvalidPrefixException("Prefix already exists as: " + k + " -> " + namespaceURI); } } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } }); if (!errors.isEmpty()) { throw new IllegalArgumentException(errors.stream().map(Exception::getMessage).collect(joining(",\n"))); } final JcrPropertyStatementListener listener = new JcrPropertyStatementListener( idTranslator, getSession(), idTranslator.reverse().convert(this).asNode()); model.register(listener); // If this resource's structural parent is an IndirectContainer, check whether the // ldp:insertedContentRelation property is present in the stream of changed triples. // If so, set the propertyChanged value to true. final AtomicBoolean propertyChanged = new AtomicBoolean(); ldpInsertedContentProperty(getNode()).ifPresent(resource -> { model.register(new PropertyChangedListener(resource, propertyChanged)); }); model.setNsPrefixes(request.getPrefixMapping()); execute(request, model); removeEmptyFragments(); listener.assertNoExceptions(); // Update the fedora:lastModified property touch(); // Update the fedora:lastModified property of the ldp:memberResource // resource, if necessary. if (propertyChanged.get()) { touchLdpMembershipResource(getNode()); } } @Override public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, final TripleCategory context) { return getTriples(idTranslator, singleton(context)); } @Override public RdfStream getTriples(final IdentifierConverter<Resource, FedoraResource> idTranslator, final Set<? extends TripleCategory> contexts) { return new DefaultRdfStream(idTranslator.reverse().convert(this).asNode(), contexts.stream() .filter(contextMap::containsKey) .map(x -> contextMap.get(x).apply(this).apply(idTranslator).apply(contexts.contains(MINIMAL))) .reduce(empty(), Stream::concat)); } /* * (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getBaseVersion() */ @Override public FedoraResource getBaseVersion() { try { return new FedoraResourceImpl(getVersionManager().getBaseVersion(getPath()).getFrozenNode()); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#isNew() */ @Override public Boolean isNew() { return node.isNew(); } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#replaceProperties * (org.fcrepo.kernel.api.identifiers.IdentifierConverter, org.apache.jena.rdf.model.Model) */ @Override public void replaceProperties(final IdentifierConverter<Resource, FedoraResource> idTranslator, final Model inputModel, final RdfStream originalTriples) throws MalformedRdfException { try (final RdfStream replacementStream = new DefaultRdfStream(idTranslator.reverse().convert(this).asNode())) { final GraphDifferencer differencer = new GraphDifferencer(inputModel, originalTriples); final StringBuilder exceptions = new StringBuilder(); try (final DefaultRdfStream diffStream = new DefaultRdfStream(replacementStream.topic(), differencer.difference())) { new RdfRemover(idTranslator, getSession(), diffStream).consume(); } catch (final ConstraintViolationException e) { throw e; } catch (final MalformedRdfException e) { exceptions.append(e.getMessage()); exceptions.append("\n"); } try (final DefaultRdfStream notCommonStream = new DefaultRdfStream(replacementStream.topic(), differencer.notCommon())) { new RdfAdder(idTranslator, getSession(), notCommonStream).consume(); } catch (final ConstraintViolationException e) { throw e; } catch (final MalformedRdfException e) { exceptions.append(e.getMessage()); } // If this resource's structural parent is an IndirectContainer, check whether the // ldp:insertedContentRelation property is present in the stream of changed triples. // If so, set the propertyChanged value to true. final AtomicBoolean propertyChanged = new AtomicBoolean(); ldpInsertedContentProperty(getNode()).ifPresent(resource -> { propertyChanged.set(differencer.notCommon().map(Triple::getPredicate).anyMatch(resource::equals)); }); removeEmptyFragments(); if (exceptions.length() > 0) { throw new MalformedRdfException(exceptions.toString()); } // Update the fedora:lastModified property touch(); // If the ldp:insertedContentRelation property was changed, update the // ldp:membershipResource resource. if (propertyChanged.get()) { touchLdpMembershipResource(getNode()); } } } private void removeEmptyFragments() { try { if (node.hasNode("#")) { @SuppressWarnings("unchecked") final Iterator<Node> nodes = node.getNode("#").getNodes(); nodes.forEachRemaining(n -> { try { @SuppressWarnings("unchecked") final Iterator<Property> properties = n.getProperties(); final boolean hasUserProps = iteratorToStream(properties).map(propertyConverter::convert) .filter(p -> !jcrProperties.contains(p)) .anyMatch(isManagedPredicate.negate()); final boolean hasUserTypes = Arrays.stream(n.getMixinNodeTypes()) .map(uncheck(NodeType::getName)).filter(hasInternalNamespace.negate()) .map(uncheck(type -> getSession().getWorkspace().getNamespaceRegistry().getURI(type.split(":")[0]))) .anyMatch(isManagedNamespace.negate()); if (!hasUserProps && !hasUserTypes && !n.getWeakReferences().hasNext() && !n.getReferences().hasNext()) { LOGGER.debug("Removing empty hash URI node: {}", n.getName()); n.remove(); } } catch (final RepositoryException ex) { throw new RepositoryRuntimeException("Error removing empty fragments", ex); } }); } } catch (final RepositoryException ex) { throw new RepositoryRuntimeException("Error removing empty fragments", ex); } } /* (non-Javadoc) * @see org.fcrepo.kernel.api.models.FedoraResource#getEtagValue() */ @Override public String getEtagValue() { final Instant lastModifiedDate = getLastModifiedDate(); if (lastModifiedDate != null) { return sha1Hex(getPath() + lastModifiedDate.toEpochMilli()); } return ""; } @Override public void enableVersioning() { try { node.addMixin("mix:versionable"); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public void disableVersioning() { try { node.removeMixin("mix:versionable"); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public boolean isVersioned() { try { return node.isNodeType("mix:versionable"); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public boolean isFrozenResource() { return isFrozenNode.test(this); } @Override public FedoraResource getVersionedAncestor() { try { if (!isFrozenResource()) { return null; } Node versionableFrozenNode = getNode(); FedoraResource unfrozenResource = getUnfrozenResource(); // traverse the frozen tree looking for a node whose unfrozen equivalent is versioned while (!unfrozenResource.isVersioned()) { if (versionableFrozenNode.getDepth() == 0) { return null; } // node in the frozen tree versionableFrozenNode = versionableFrozenNode.getParent(); // unfrozen equivalent unfrozenResource = new FedoraResourceImpl(versionableFrozenNode).getUnfrozenResource(); } return new FedoraResourceImpl(versionableFrozenNode); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public FedoraResource getUnfrozenResource() { if (!isFrozenResource()) { return this; } try { // Either this resource is frozen if (hasProperty(JCR_FROZEN_UUID)) { try { return new FedoraResourceImpl(getNodeByProperty(getProperty(JCR_FROZEN_UUID))); } catch (final ItemNotFoundException e) { // The unfrozen resource has been deleted, return the tombstone. return new TombstoneImpl(getNode()); } // ..Or it is a child-version-history on a frozen path } else if (hasProperty(JCR_CHILD_VERSION_HISTORY)) { final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY)); try { final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID)); return new FedoraResourceImpl(childNode); } catch (final ItemNotFoundException e) { // The unfrozen resource has been deleted, return the tombstone. return new TombstoneImpl(childVersionHistory); } } else { throw new RepositoryRuntimeException("Resource must be frozen or a child-history!"); } } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public FedoraResource getVersion(final String label) { try { final Node n = getFrozenNode(label); if (n != null) { return new FedoraResourceImpl(n); } if (isVersioned()) { final VersionHistory hist = getVersionManager().getVersionHistory(getPath()); if (hist.hasVersionLabel(label)) { LOGGER.debug("Found version for {} by label {}.", this, label); return new FedoraResourceImpl(hist.getVersionByLabel(label).getFrozenNode()); } } LOGGER.warn("Unknown version {} with label {}!", getPath(), label); return null; } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public Stream<FedoraVersion> getVersions() { try { final VersionHistory history = getVersionManager().getVersionHistory(getPath()); @SuppressWarnings("unchecked") final Iterator<Version> versions = history.getAllVersions(); return iteratorToStream(versions) /* discard jcr:rootVersion */ .filter(UncheckedPredicate.uncheck(version -> !version.getName().equals(history.getRootVersion().getName()))) /* omit unlabelled versions */ .filter(UncheckedPredicate.uncheck(version -> { final String[] labels = history.getVersionLabels(version); if (labels.length == 0) { LOGGER.warn("An unlabelled version for {} was found! Omitting from version listing!", getPath()); } else if (labels.length > 1) { LOGGER.warn("Multiple version labels found for {}! Using first label, \"{}\".", getPath(), labels[0]); } return labels.length > 0; })) .map(uncheck(version -> new FedoraVersionImpl(history.getVersionLabels(version)[0], version.getCreated().toInstant()))); } catch (final RepositoryException ex) { throw new RepositoryRuntimeException(ex); } } @Override public String getVersionLabelOfFrozenResource() { if (!isFrozenResource()) { return null; } // Frozen node is required to find associated version label final Node frozenResource; try { // Version History associated with this resource final VersionHistory history = getVersionManager().getVersionHistory(getUnfrozenResource().getPath()); // Possibly the frozen node is nested inside of current child-version-history if (getNode().hasProperty(JCR_CHILD_VERSION_HISTORY)) { final Node childVersionHistory = getNodeByProperty(getProperty(JCR_CHILD_VERSION_HISTORY)); final Node childNode = getNodeByProperty(childVersionHistory.getProperty(JCR_VERSIONABLE_UUID)); final Version childVersion = getVersionManager().getBaseVersion(childNode.getPath()); frozenResource = childVersion.getFrozenNode(); } else { frozenResource = getNode(); } // Loop versions @SuppressWarnings("unchecked") final Stream<Version> versions = iteratorToStream(history.getAllVersions()); return versions .filter(UncheckedPredicate.uncheck(version -> version.getFrozenNode().equals(frozenResource))) .map(uncheck(history::getVersionLabels)) .flatMap(Arrays::stream) .findFirst().orElse(null); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } private Node getNodeByProperty(final Property property) throws RepositoryException { return getSession().getNodeByIdentifier(property.getString()); } protected VersionManager getVersionManager() { try { return getSession().getWorkspace().getVersionManager(); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } private static Collection<IllegalArgumentException> validateUpdateRequest(final UpdateRequest request) { return request.getOperations().stream() .flatMap(x -> { if (x instanceof UpdateModify) { final UpdateModify y = (UpdateModify) x; return concat(y.getInsertQuads().stream(), y.getDeleteQuads().stream()); } else if (x instanceof UpdateData) { return ((UpdateData) x).getQuads().stream(); } else if (x instanceof UpdateDeleteWhere) { return ((UpdateDeleteWhere) x).getQuads().stream(); } else { return empty(); } }) .flatMap(FedoraResourceImpl::validateQuad) .filter(x -> x != null) .collect(Collectors.toList()); } private static Stream<IllegalArgumentException> validateQuad(final Quad quad) { return quadValidators.stream().map(x -> x.apply(quad)); } private Node getFrozenNode(final String label) throws RepositoryException { try { final Session session = getSession(); final Node frozenNode = session.getNodeByIdentifier(label); final String baseUUID = getNode().getIdentifier(); /* * We found a node whose identifier is the "label" for the version. Now * we must do due dilligence to make sure it's a frozen node representing * a version of the subject node. */ final Property p = frozenNode.getProperty(JCR_FROZEN_UUID); if (p != null) { if (p.getString().equals(baseUUID)) { return frozenNode; } } /* * Though a node with an id of the label was found, it wasn't the * node we were looking for, so fall through and look for a labeled * node. */ } catch (final ItemNotFoundException ex) { /* * the label wasn't a uuid of a frozen node but * instead possibly a version label. */ } return null; } @Override public boolean equals(final Object object) { if (object instanceof FedoraResourceImpl) { return ((FedoraResourceImpl) object).getNode().equals(this.getNode()); } return false; } @Override public int hashCode() { return getNode().hashCode(); } protected Session getSession() { try { return getNode().getSession(); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } @Override public String toString() { return getNode().toString(); } protected Property getProperty(final String relPath) { try { return getNode().getProperty(relPath); } catch (final RepositoryException e) { throw new RepositoryRuntimeException(e); } } /** * A method that takes a Triple and returns a Triple that is the correct representation of * that triple for the given resource. The current implementation of this method is used by * {@link PropertiesRdfContext} to replace the reported {@link org.fcrepo.kernel.api.RdfLexicon#LAST_MODIFIED_DATE} * with the one produced by {@link #getLastModifiedDate}. * @param r the Fedora resource * @param translator a converter to get the external identifier from a jcr node * @return a function to convert triples */ public static Function<Triple, Triple> fixDatesIfNecessary(final FedoraResource r, final Converter<Node, Resource> translator) { return t -> { if (t.getPredicate().toString().equals(LAST_MODIFIED_DATE.toString()) && t.getSubject().equals(translator.convert(getJcrNode(r)).asNode())) { final Calendar c = new Calendar.Builder().setInstant(r.getLastModifiedDate().toEpochMilli()).build(); return new Triple(t.getSubject(), t.getPredicate(), createTypedLiteral(c).asNode()); } return t; }; } }